/*

 setbrightness - sets your laptop's screen brightness (backlight) from the
 command-line.

 This program does nothing more than open a file and write some specific
 numerical value to it, obtained from the command-line.

 Only that this file will be the backlight brightness file, thus allowing us
 to set it with sudo, or even without it via setuid ;)

 Copyright 2022 kzimmermann - https://tilde.town/~kzimmermann/
 This program is Free Software licensed under the GNU GPLv3 or later.
 For more information, please see https://www.gnu.org/licenses

 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Alpine Linux with intel chips sets this file here. Change as needed.
const char *BRIGHTFILE = "/sys/class/backlight/intel_backlight/brightness";

// Likewise, this is the location of the file indicating max value for it:
const char *MAX_BRIGHTFILE = "/sys/class/backlight/intel_backlight/max_brightness";

void
helper(char *progname) {
    printf("%s - utility to set the laptop's display brightness (backlight) from the command-line\n", progname);
    printf("Usage: %s BRIGHTNESS [+BRIGHTNESS] [-BRIGHTNESS]\n", progname);
    printf("Where BRIGHTNESS is an absolute value between 1 and 100, or a 'step' value preceeded by a + (increase) or a - (decrease) sign.\n");
    printf("Examples:\n");
    printf("%s 50 # sets the display brightness to 50%%\n", progname);
    printf("%s +10 # sets the brightness 10%% brighter\n", progname);
    printf("%s -15 # sets the brightness 15%% dimmer\n", progname);
}

// datatype of a "tuple" containing a leading character and a numerical value:
struct 
argtuple {
    char sign;
    int value;
};

// this function will process a token like "+83239" and identify the parts:
struct argtuple
parseArg(char token[])
{
    struct argtuple t;
    sscanf(token, "%1c%d", &t.sign, &t.value);
    if (!(t.sign == '+' || t.sign == '-')) {
        // parsing error. Let's use the (!,0) tuple as an error code
        t.sign = '!';
        t.value = 0;
    }
    return t;
}

int 
main(int argc, char *argv[])
{
    int status = 0;
    int maxbrightness = 0;
    int brightness = 0;
    int is_absolute = 0;
    int multiplier = 1;
    struct argtuple at;
    FILE *fp;

    // parse -h or --help argument:
    if (argc == 2) {
        if (strncmp(argv[1], "-h", 2) == 0 || strncmp(argv[1], "--help", 6) == 0) {
            helper(argv[0]);
            return 0;
        }
    }

    fp = fopen(BRIGHTFILE, "r");
    if (fp == NULL) {
        printf("Error: brightness file does not exist.\n");
        printf("Make sure some sort of brightness file exists under '/sys', edit it in the source and try again.\n");
        return 1;
    }
    else {
        fscanf(fp, "%d", &brightness);
        status = fclose(fp);
    }
    
    // Now that the file indeed exists, get the maximum brightness:
    fp = fopen(MAX_BRIGHTFILE, "r");
    if (fp == NULL) {
        printf("Error: maximum brightness file does not exist.\n");
        printf("Make sure this file exists under '/sys', edit it in the source and try again.\n");
        return 1;
    }
    else {
        // Read the maximum possible value from it:
        fscanf(fp, "%d", &maxbrightness);
        status = fclose(fp);
    }

    // Read command-line arguments:
    if (argc == 1) {
        printf("The current brightness is %d, or %.1f%%\n", brightness, 100.0*brightness / maxbrightness);
        printf("Pass a value from 1 to 100 to set the new brightness.\n");
        return 0;
    }

    // Scan the input, see if the chars '-' or '+' are present. If they are,
    // we will set the brightness relative to the current (respecting limits)
    at = parseArg(argv[1]);
    if (at.sign == '!') {
        printf("[DEBUG] Setting *absolute* brightness\n");
        is_absolute = 1;
    }
    else {
        printf("[DEBUG] Setting *relative* brightness\n");
        is_absolute = 0;
    }

    // Here's now how we actually do the logic of it: the user will pass a
    // number between 1 and 100 as $1 and from this, we will calculate a 
    // percentage of the maximum allowed brightness. If the boundaries are
    // respected (and the value passed is actually a digit, not a string), 
    // then we overwrite the system's brightness file with that safe value.

    if (is_absolute == 1) {
        multiplier = atoi(argv[1]);

        // integer parsing errors are expressed as a 0, so we can check 
        // the bounds:
        if (multiplier > 100 || multiplier < 1) {
            printf("Error: pass a number from 1 to 100 to set new brightness.\n");
            return 1;
        }

        // If everything is within bounds, then:
        brightness = (int)((0.01 * multiplier) * maxbrightness);
    }
    else { 
        // relative change. Get the "int" version of percentual brightness:
        multiplier = (int)((100.0 * brightness) / maxbrightness);
        if (at.sign == '-') 
            multiplier -= at.value;
        else
            multiplier += at.value;

        if (multiplier <= 0) {
            printf("Cannot set brightness lower than 0. Already at minimum.\n");
            multiplier = 2;
        }
        else if (multiplier >= 100) {
            printf("Cannot set brightness higher than 100. Already at max.\n");
            multiplier = 100;
        }
    }

    // If everything is within bounds, then:
    brightness = (int)((0.01 * multiplier) * maxbrightness);

    fp = fopen(BRIGHTFILE, "w");
    if (fp == NULL) {
        printf("Error: could not open the brightness file for writing.\n");
        printf("Try it as root or with sudo as member of the 'video' group.\n");
        return 1;
    }
    else {
        fprintf(fp, "%d\n", brightness);
        status = fclose(fp);
        printf("The new brightness is %d, or %d%%\n", brightness, multiplier);
    }

    return 0;
}
